package scaladuct

import collection.immutable.List
import scala.collection.mutable
import collection.JavaConversions.JMapWrapper
import org.apache.commons.convert.{Converter, Converters}
import java.lang.{Iterable, Class, String}
import java.lang.reflect.{Type, ParameterizedType}
import java.text.{ParseException, SimpleDateFormat}
import util.GenericAnalysisUtils
import java.util.{ArrayList, Collections, Date, EnumSet}

/**
 * <p>
 * Created by IntelliJ IDEA.
 * User: zslajchrt
 * Date: Aug 7, 2010
 * Time: 12:54:17 PM
 */

object CookieBeanFactory {
  def apply(cookie: AnyRef): CookieBean[AnyRef] = create(cookie)

  def create(cookie: AnyRef): CookieBean[AnyRef] = {
    cookie match {
      case s: java.lang.String => new ScalarCookieBean[AnyRef](s)
      case n: java.lang.Number => new ScalarCookieBean[AnyRef](n)
      case c: java.lang.Character => new ScalarCookieBean[AnyRef](c)
      case b: java.lang.Boolean => new ScalarCookieBean[AnyRef](b)
      case ar: AnyRef => new CompositeCookieBean[AnyRef](ar)
    }
  }
}

object PropertyUtils {

  def containsContextReferencesOnly(cookie: AnyRef): Boolean = {
    val cookieBean = CookieBeanFactory(cookie)
    cookieBean.props.forall(p => {
      p.refTypeInfo != null && p.refTypeInfo.name != null && p.refTypeInfo.name != ""
    })
  }

  def convertToProperty[X](obj: X): Property[X] = {

    new Property[X] {
      def stringify() = obj.toString

      def clazz[Y >: X] = obj.asInstanceOf[AnyRef].getClass().asInstanceOf[Class[Y]]

      def typeInfo = null

      def refTypeInfo = null

      def value = obj

      def name = obj.toString
    }
  }

  implicit def convertToListOfProperties(list: List[Any]): List[Property[Any]] = {
    list.map(elem => convertToPropertyImplicit(elem))
  }

  implicit def convertToPropertyImplicit(obj: Any): Property[Any] = {
    convertToProperty(obj)
  }

  def extractClassFromGenericType(genericParameters: java.lang.reflect.Type): Option[Array[Type]] = {
    if (genericParameters == null)
      None
    else
      genericParameters match {
        case parType: ParameterizedType => {
          //          val genericClasses: Array[Class[Any]] = parType.getActualTypeArguments().map((elementType) => {
          //            elementType match {
          //              case elementClass: Class[Any] => elementClass
          //              case _ => error("Unsupported element parameterized type " + elementType)
          //            }
          //          })
          //          Some(genericClasses)
          Some(parType.getActualTypeArguments)
        }
        //case _ => error("Unsupported element type " + genericParameters)
        case _ => None
      }
  }

}

@serializable
@SerialVersionUID(1L)
trait Property[+X] extends Extensible {
  def name: String

  def value: X

  /**
   * If this property represents a named reference the return value
   * provides a type information about the reference's target.
   * Otherwise the return value is null.
   */
  def refTypeInfo: RefInfo

  /**
   * The complete type information about this property.
   */
  def typeInfo: TypeInfo

  /**
   * The raw class of this property's value.
   */
  def clazz[Y >: X]: Class[Y]

  def stringify(): String

}

trait CompactProperty extends Property[CompactProperty] {
  def stringify() = name

  def clazz[Y >: CompactProperty] = getClass.asInstanceOf[Class[Y]]

  def typeInfo = null

  def refTypeInfo = null

  def value = this

  def name = toString
}

trait MutableProperty[X] extends Property[X] {
  def update(newValue: X): Unit

  def parse(source: String): Unit = {
    parse(Collections.singletonList(source));
  }

  def parse(source: java.lang.Iterable[String]): Unit
}

object ImmutableProperty {
  def apply(name: String): ImmutableProperty[AnyRef] = apply(name, null, classOf[AnyRef])

  def apply[X <: AnyRef](name: String, value: X): ImmutableProperty[X] = new PropertyValue[X](name, value)

  def apply[X <: AnyRef](name: String, value: X, clazz: Class[_]): PropertyValue[X] = new PropertyValue[X](name, value, null, clazz.asInstanceOf[Class[X]])
}

trait ImmutableProperty[+X] extends Property[X] {
  def update[Y >: X <: AnyRef](newValue: Y): ImmutableProperty[Y]

  def parse(source: String): ImmutableProperty[X]

  def parse(source: java.lang.Iterable[String]): ImmutableProperty[X]
}

class MutablePropertyValue[X](val name: String, private[this] var v: X,
                              val cls: Class[X], tInfo: TypeInfo, rInfo: RefInfo) extends MutableProperty[X] {

  //val genericParameters: Option[Array[Type]] = PropertyUtils.extractClassFromGenericType(genericParameterTypes)
  //val genericParameters: RefInfo = tInfo

  def typeInfo = tInfo

  def refTypeInfo = rInfo

  def clazz[Y >: X] = cls.asInstanceOf[Class[Y]]

  def this(name: String, v: X, clazz: Class[X]) = this (name, v, clazz, null, null)

  def update(newValue: X) = v = newValue

  def refAnnot = null

  def value = v

  def stringify(): String = {
    AuxConverter.stringify(clazz, value)
  }

  def parse(source: Iterable[String]) = {
    try {
      update(AuxConverter.parse(clazz, source, value, typeInfo))
    }
    catch {
      case e: Exception => throw new IllegalArgumentException("Problem with parsing value for property " + name, e)
    };
  }

}

object AuxConverter {
  def stringify[X](clazz: Class[X], value: X): String = {
    if (clazz == classOf[Date]) {
      DateFormat.format.format(value)
    } else {
      val converterToString: Converter[X, String] =
        Converters.getConverter(clazz, classOf[String])
      converterToString.convert(value)
    }
  }

  private def concatenate(sourceIter: java.util.Iterator[String]): String = {
    val strBldr = new StringBuilder();
    while (sourceIter.hasNext) {
      strBldr.append(sourceIter.next);
    }
    strBldr.toString
  }

  def parse[X](clazz: Class[X], source: java.lang.Iterable[String], currentValue: X,
               typeInfo: TypeInfo): X = {
    if (classOf[java.util.List[_]].isAssignableFrom(clazz) && typeInfo != null) {
      val elementClass = try {
        GenericAnalysisUtils.getRawClassFromType(
          GenericAnalysisUtils.getParameterType(typeInfo.getType, 0)).asInstanceOf[Class[Any]]
      } catch {
        case e: Exception => classOf[String]
      }

      val currentList = if (currentValue != null)
        currentValue.asInstanceOf[java.util.Collection[Any]]
      else
        new ArrayList[Any]();
      currentList.clear

      val iter = source.iterator
      while (iter.hasNext) {
        val elementAsString = iter.next
        val element = parse(elementClass, Collections.singletonList(elementAsString), null, null)
        currentList.add(element)
      }

      currentList.asInstanceOf[X]
    } else
      parseScalar(clazz, concatenate(source.iterator), currentValue, typeInfo)
  }

  def parseScalar[X](clazz: Class[X], source: String, currentValue: X, typeInfo: TypeInfo): X = {
    if (clazz == classOf[Date]) {
      try {
        // try the date format
        DateFormat.formatDate.parse(source).asInstanceOf[X]
      }
      catch {
        case pe: ParseException => {
          // try the timestamp format
          DateFormat.format.parse(source).asInstanceOf[X]
        }
      }
    } else if (clazz.isEnum) {
      def convertEnum[T <: Enum[T]]() = Enum.valueOf(clazz.asInstanceOf[Class[T]], source)
      convertEnum()
    } else {
      val converter = Converters.getConverter(classOf[String], clazz);
      converter.convert(source)
    }
  }
}

class PropertyValue[+X <: AnyRef](
                                   val name: String,
                                   val value: X,
                                   cls: Class[X],
                                   tInfo: TypeInfo,
                                   rInfo: RefInfo) extends ImmutableProperty[X] {

  val isReference = refTypeInfo != null

  def typeInfo = tInfo

  def refTypeInfo = rInfo

  def this(name: String, value: X, ref: Ref, clazz: Class[X]) = this (name, value, clazz, PhaseMethodRefInfo.newInstance(ref), null);

  def this(name: String, value: X, particleRefAnnotOption: Option[Ref]) = this (name, value, {
    particleRefAnnotOption match {
      case None => null
      case Some(particleRefAnnot) => particleRefAnnot
    }
  }, value.asInstanceOf[Class[X]])

  def this(name: String, value: X) = this (name, value, null, value.getClass.asInstanceOf[Class[X]])

  def clazz[Y >: X] = cls.asInstanceOf[Class[Y]]

  def apply(): X = value

  def update[Y >: X <: AnyRef](newValue: Y) = {
    new PropertyValue[Y](name, newValue)
  }

  def stringify(): String = {
    AuxConverter.stringify(clazz, value)
  }

  def parse(source: Iterable[String]): PropertyValue[X] = {
    try {
      update(AuxConverter.parse(clazz, source, value, typeInfo))
    }
    catch {
      case e: Exception => throw new IllegalArgumentException("Problem with parsing value for property " + name, e)
    };
  }

  def parse(source: String): PropertyValue[X] = {
    parse(Collections.singletonList(source));
  }

  override def toString = value + ":" + clazz + " [ref:" + refTypeInfo + "]"
}

object DateFormat {
  val format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
  val formatDate = new SimpleDateFormat("M/d/yyyy")
}

trait CookieBean[X <: AnyRef] {
  def props(): List[PropertyValue[AnyRef]]

  def property(name: String): Option[PropertyValue[AnyRef]] = props.find(_.name == name)

  def cookie(): X

  def update(newProps: List[PropertyValue[AnyRef]]): CookieBean[X]
}

class ImmutableCookieBean(val cookie: AnyRef) extends CookieBean[AnyRef] {
  def props() = List.empty

  def update(newProps: List[PropertyValue[AnyRef]]) = this
}

class ScalarCookieBean[X <: AnyRef](val cookie: X) extends CookieBean[X] {
  private[this] val p = new PropertyValue[AnyRef]("p", cookie)

  def props() = List[PropertyValue[AnyRef]](p)

  def update(newProps: List[PropertyValue[AnyRef]]) = {
    val newProp :: rest = newProps
    val newValue: AnyRef = newProp.apply
    new ScalarCookieBean[X](newValue.asInstanceOf[X])
  }
}

class CompositeCookieBean[X <: AnyRef](val cookie: X) extends CookieBean[X] {
  val cookieInfo: BeanInfo = BeanUtils.createBeanInfo(cookie)

  def update(newProps: java.util.List[PropertyValue[AnyRef]]): CompositeCookieBean[X] = {
    val newPropsArr = newProps.toArray().map(_.asInstanceOf[PropertyValue[AnyRef]])
    var newPropsSL = List(newPropsArr: _*)
    update(newPropsSL)
  }

  def update(newProps: List[PropertyValue[AnyRef]]): CompositeCookieBean[X] = {
    newProps.foreach(p => {
      cookieInfo.asMap.put(p.name, p.value)
    })
    new CompositeCookieBean[X](cookie)
  }

  def props() = {
    var plist = List[PropertyValue[AnyRef]]()
    for ((k, v) <- cookieInfo.asMap) {
      val propName = k.asInstanceOf[String]
      val propValue = v.asInstanceOf[AnyRef]
      val refAnnot: Ref = cookieInfo.propertyAnnotation(propName, classOf[Ref])
      val typeInfo = new BeanInfoRefInfo(cookieInfo, propName)

      //      plist = new PropertyValue[AnyRef](propName, propValue,
      //        cookieInfo.propertyType(propName).asInstanceOf[Class[AnyRef]],
      //        typeInfo, PhaseMethodRefInfo.newInstance(refAnnot)) :: plist
      plist = new PropertyValue[AnyRef](propName, propValue,
        cookieInfo.propertyType(propName).asInstanceOf[Class[AnyRef]],
        typeInfo, RefInfoFactory.newInstance(refAnnot, cookieInfo, propName)) :: plist
    }

    plist.reverse
  }
}